import cppyy
import cffi
from bullet import bt
import inspect

ffi = cffi.FFI()

if cppyy.gbl._py_BT_USE_DOUBLE_PRECISION:
    ffi.cdef("typedef double btScalar;")
else:
    ffi.cdef("typedef float btScalar;")

class GlobalCallback (object):
    cffi_signature = None

    def __init__(self):
        self.cffi_cbobj = ffi.callback(self.cffi_signature, self.cffi_callback)
        self.func = None

    def __get__(self, obj, objtype):
        return self.func

    def __set__(self, obj, value):
        self.func = value
        if value:
            cb = int(ffi.cast("intptr_t", self.cffi_cbobj))
        else:
            cb = 0
        getattr(cppyy.gbl, self.cpp_setter)(cb)


class ContactAddedCallback (GlobalCallback):
    cpp_setter = "_py_set_gContactAddedCallback"
    cffi_signature = "bool (*f)(intptr_t i_cp, intptr_t i_colObj0, int partId0, int index0, intptr_t i_colObj1, int partId1, int index1)"

    def cffi_callback(self, i_cp, i_colObj0, partId0, index0, i_colObj1,
                      partId1, index1):
        cp = cppyy.bind_object(i_cp, bt.ManifoldPoint)
        colObj0 = cppyy.bind_object(i_colObj0, bt.CollisionObjectWrapper)
        colObj1 = cppyy.bind_object(i_colObj1, bt.CollisionObjectWrapper)
        return bool(self.func(cp, colObj0, partId0, index0, colObj1, partId1, index1))


class ContactProcessedCallback (GlobalCallback):
    cpp_setter = "_py_set_gContactProcessedCallback"
    cffi_signature = "bool (*f)(intptr_t i_cp, intptr_t i_body0, intptr_t i_body1)"

    def cffi_callback(self, i_cp, i_body0, i_body1):
        cp = cppyy.bind_object(i_cp, bt.ManifoldPoint)
        body0 = cppyy.bind_object(i_body0, bt.CollisionObject)
        body1 = cppyy.bind_object(i_body1, bt.CollisionObject)
        return bool(self.func(cp, body0, body1))


class ContactDestroyedCallback (GlobalCallback):
    cpp_setter = "_py_set_gContactDestroyedCallback"
    cffi_signature = "bool (*f)(intptr_t i_userPersistentData)"

    def cffi_callback(self, i_userPersistentData):
        return bool(self.func(i_userPersistentData))


# This is a hack, but there doesn't seem to be any better way to get a
# reference to cppyy.CppyyClassMeta to make our own submetaclass..
CppyyClassMeta = cppyy.gbl.btVector3.__metaclass__

class CallbackMeta (CppyyClassMeta):
    def __new__(meta, name, bases, classdict):
        print (CallbackMeta, meta)
        #newclass = super(CallbackMeta, meta).__new__(meta, name, bases, classdict)
        newclass = CppyyClassMeta.__new__(meta, name, bases, classdict)

        for fname in newclass._callback_methods.keys():
            if fname not in classdict:

                def notimplemented(self, *args, **kwargs):
                    raise NotImplementedError("{}.{} is not defined".format(self.__class__.__name__, fname))

                setattr(newclass, fname, notimplemented)

        def __new__(cls):
            o = super(newclass, cls).__new__(cls)
            o.__class__ = cls
            cbobjs = []
            for fname, proto in cls._callback_methods.items():
                try:
                    cb_method = getattr(o, '_cffi_' + fname)
                except AttributeError:
                    cb_method = getattr(o, fname)
                cbobj = ffi.callback(proto, cb_method)
                setattr(o, '_{}_ptr'.format(fname), int(ffi.cast("intptr_t", cbobj)))
                cbobjs.append(cbobj)
            o._callback_objs = cbobjs
            return o

        newclass.__new__ = classmethod(__new__)
        return newclass


class btTriangleCallback (cppyy.gbl._py_btTriangleCallback):
    def __new__(cls):
        o = cppyy.gbl._py_btTriangleCallback.__new__(cls)
        o.__class__ = cls
        return o

    def __init__(self):
        self._cffi_cbobj = ffi.callback("void (*f)(intptr_t, intptr_t, intptr_t, int, int)", self._cffi_processTriangle)
        self._processTriangle_ptr = int(ffi.cast("intptr_t", self._cffi_cbobj))

    def _cffi_processTriangle(self, i_triangle0, i_triangle1, i_triangle2, partId, triangleIndex):
        triangle0 = cppyy.bind_object(i_triangle0, bt.Vector3)
        triangle1 = cppyy.bind_object(i_triangle1, bt.Vector3)
        triangle2 = cppyy.bind_object(i_triangle2, bt.Vector3)
        self.processTriangle((triangle0, triangle1, triangle2), partId, triangleIndex)

    def __repr__(self):
        return "<{} object at 0x{:x}>".format(self.__class__.__name__, cppyy.addressof(self))


class btInternalTriangleIndexCallback (cppyy.gbl._py_btInternalTriangleIndexCallback):
    def __new__(cls):
        o = cppyy.gbl._py_btInternalTriangleIndexCallback.__new__(cls)
        o.__class__ = cls
        return o

    def __init__(self):
        self._cffi_cbobj = ffi.callback("void (*f)(intptr_t, intptr_t, intptr_t, int, int)", self._cffi_internalProcessTriangleIndex)
        self._internalProcessTriangleIndex_ptr = int(ffi.cast("intptr_t", self._cffi_cbobj))

    def _cffi_internalProcessTriangleIndex(self, i_triangle0, i_triangle1, i_triangle2, partId, triangleIndex):
        triangle0 = cppyy.bind_object(i_triangle0, bt.Vector3)
        triangle1 = cppyy.bind_object(i_triangle1, bt.Vector3)
        triangle2 = cppyy.bind_object(i_triangle2, bt.Vector3)
        self.internalProcessTriangleIndex((triangle0, triangle1, triangle2), partId, triangleIndex)

    def __repr__(self):
        return "<{} object at 0x{:x}>".format(self.__class__.__name__, cppyy.addressof(self))


class btIDebugDraw (cppyy.gbl._py_btIDebugDraw):
    _callback_methods = {
        'drawLine': "void (*f)(intptr_t, intptr_t, intptr_t, intptr_t)",
        'drawSphere_t': "void (*f)(btScalar, intptr_t, intptr_t)",
        'drawSphere_p': "void (*f)(intptr_t, btScalar, intptr_t)",
        'drawTriangle': "void (*f)(intptr_t, intptr_t, intptr_t, intptr_t, intptr_t, intptr_t, intptr_t, btScalar)",
        'drawContactPoint': "void (*f)(intptr_t, intptr_t, btScalar, int, intptr_t)",
        'reportErrorWarning': "void (*f)(const char *)",
        'draw3dText': "void (*f)(intptr_t, const char *)",
        'setDebugMode': "void (*f)(int)",
        'getDebugMode': "int (*f)(void)",
        'drawAabb': "void (*f)(intptr_t, intptr_t, intptr_t)",
        'drawTransform': "void (*f)(intptr_t, btScalar)",
        'drawArc': "void (*f)(intptr_t, intptr_t, intptr_t, btScalar, btScalar, btScalar, btScalar, intptr_t, bool, btScalar)",
        'drawSpherePatch': "void (*f)(intptr_t, intptr_t, intptr_t, btScalar, btScalar, btScalar, btScalar, btScalar, intptr_t, btScalar)",
        'drawBox': "void (*f)(intptr_t, intptr_t, intptr_t, intptr_t)",
        'drawCapsule': "void (*f)(btScalar, btScalar, int, intptr_t, intptr_t)",
        'drawCylinder': "void (*f)(btScalar, btScalar, int, intptr_t, intptr_t)",
        'drawCone': "void (*f)(btScalar, btScalar, int, intptr_t, intptr_t)",
        'drawPlane': "void (*f)(intptr_t, btScalar, intptr_t, intptr_t)",
    }

    def __repr__(self):
        return "<{} object at 0x{:x}>".format(self.__class__.__name__, cppyy.addressof(self))

    def __new__(cls):
        o = super(btIDebugDraw, cls).__new__(cls)
        o.__class__ = cls
        cbobjs = []
        for fname, proto in cls._callback_methods.items():
            try:
                cb_method = getattr(o, '_cffi_' + fname)
            except AttributeError:
                cb_method = getattr(o, fname)
            cbobj = ffi.callback(proto, cb_method)
            setattr(o, '_{}_ptr'.format(fname), int(ffi.cast("intptr_t", cbobj)))
            cbobjs.append(cbobj)
        o._callback_objs = cbobjs
        return o

    ###########################################################################
    # Callback wrappers (called by the corresponding routines in
    # _py_btIDebugDraw (C++) back into Python, by way of CFFI)
    ###########################################################################

    def _cffi_drawLine(self, i_from, i_to, i_fromColor, i_toColor):
        vfrom = cppyy.bind_object(i_from, bt.Vector3)
        vto = cppyy.bind_object(i_to, bt.Vector3)
        fromColor = cppyy.bind_object(i_fromColor, bt.Vector3)
        if i_toColor:
            toColor = cppyy.bind_object(i_toColor, bt.Vector3)
            self.drawLine(vfrom, vto, fromColor, toColor)
        else:
            self.drawLine(vfrom, vto, fromColor)

    def _cffi_drawSphere_t(self, radius, i_transform, i_color):
        transform = cppyy.bind_object(i_transform, bt.Transform)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawSphere_t(radius, transform, color)

    def _cffi_drawSphere_p(self, i_p, radius, i_color):
        p = cppyy.bind_object(i_p, bt.Vector3)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawSphere_p(p, radius, color)

    def _cffi_drawTriangle(self, i_v0, i_v1, i_v2, i_n0, i_n1, i_n2, i_color, alpha):
        v0 = cppyy.bind_object(i_v0, bt.Vector3)
        v1 = cppyy.bind_object(i_v1, bt.Vector3)
        v2 = cppyy.bind_object(i_v2, bt.Vector3)
        if i_n0:
            n0 = cppyy.bind_object(i_n0, bt.Vector3)
            n1 = cppyy.bind_object(i_n1, bt.Vector3)
            n2 = cppyy.bind_object(i_n2, bt.Vector3)
        else:
            n0 = n1 = n2 = None
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawTriangle(v0, v1, v2, n0, n1, n2, color, alpha)

    def _cffi_drawContactPoint(self, i_pointOnB, i_normalOnB, distance, lifetime, i_color):
        pointOnB = cppyy.bind_object(i_pointOnB, bt.Vector3)
        normalOnB = cppyy.bind_object(i_normalOnB, bt.Vector3)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawContactPoint(pointOnB, normalOnB, distance, lifetime, color)

    def _cffi_reportErrorWarning(self, warningString):
        warningString = ffi.string(warningString)
        self.reportErrorWarning(warningString)

    def _cffi_draw3dText(self, i_location, textString):
        location = cppyy.bind_object(i_location, bt.Vector3)
        textString = ffi.string(textString)
        self.draw3dText(location, textString)

    def _cffi_drawAabb(self, i_from, i_to, i_color):
        vfrom = cppyy.bind_object(i_from, bt.Vector3)
        vto = cppyy.bind_object(i_to, bt.Vector3)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawAabb(vfrom, vto, color)

    def _cffi_drawTransform(self, i_transform, orthoLen):
        transform = cppyy.bind_object(i_transform, bt.Transform)
        self.drawTransform(transform, orthoLen)

    def _cffi_drawArc(self, i_center, i_normal, i_axis, radiusA, radiusB, minAngle, maxAngle, i_color, drawSect, stepDegrees):
        center = cppyy.bind_object(i_center, bt.Vector3)
        normal = cppyy.bind_object(i_normal, bt.Vector3)
        axis = cppyy.bind_object(i_axis, bt.Vector3)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawArc(center, normal, axis, radiusA, radiusB, minAngle, maxAngle, color, drawSect, stepDegrees)

    def _cffi_drawSpherePatch(self, i_center, i_up, i_axis, radius, minTh, maxTh, minPs, maxPs, i_color, stepDegrees):
        center = cppyy.bind_object(i_center, bt.Vector3)
        up = cppyy.bind_object(i_up, bt.Vector3)
        axis = cppyy.bind_object(i_axis, bt.Vector3)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawSpherePatch(center, up, axis, radius, minTh, maxTh, minPs, maxPs, color, stepDegrees)

    def _cffi_drawBox(self, i_bbMin, i_bbMax, i_color, i_trans):
        bbMin = cppyy.bind_object(i_bbMin, bt.Vector3)
        bbMax = cppyy.bind_object(i_bbMax, bt.Vector3)
        color = cppyy.bind_object(i_color, bt.Vector3)
        if i_trans:
            trans = cppyy.bind_object(i_trans, bt.Transform)
            self.drawBox(bbMin, bbMax, color, trans)
        else:
            self.drawBox(bbMin, bbMax, color)

    def _cffi_drawCapsule(self, radius, halfHeight, upAxis, i_transform, i_color):
        transform = cppyy.bind_object(i_transform, bt.Transform)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawCapsule(radius, halfHeight, upAxis, transform, color)

    def _cffi_drawCylinder(self, radius, halfHeight, upAxis, i_transform, i_color):
        transform = cppyy.bind_object(i_transform, bt.Transform)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawCylinder(radius, halfHeight, upAxis, transform, color)

    def _cffi_drawCone(self, radius, height, upAxis, i_transform, i_color):
        transform = cppyy.bind_object(i_transform, bt.Transform)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawCone(radius, height, upAxis, transform, color)

    def _cffi_drawPlane(self, i_planeNormal, planeConst, i_transform, i_color):
        planeNormal = cppyy.bind_object(i_planeNormal, bt.Vector3)
        transform = cppyy.bind_object(i_transform, bt.Transform)
        color = cppyy.bind_object(i_color, bt.Vector3)
        self.drawPlane(planeNormal, planeConst, transform, color)

    ###########################################################################
    # Base (default) implementations, either inherited from the base class (by
    # way of wrapper functions in _py_btIDebugDraw), or if not implemented in
    # the base class, we implement stubs that raise NotImplementedError.
    ###########################################################################

    def drawLine(self, vfrom, vto, fromColor, toColor=None):
        raise NotImplementedError()

    drawSphere_t = cppyy.gbl._py_btIDebugDraw._drawSphere_t

    drawSphere_p = cppyy.gbl._py_btIDebugDraw._drawSphere_p

    # NOTE: This is an optimization:  The default btIDebugDraw::drawTriangle
    # with normals implementation just calls drawTriangle again without
    # normals, which results in another pass through the whole C++->Python->C++
    # mess.  We'll just skip that and call the without-normals base method
    # in all cases.
    # (This results in the same behavior, but is technically incorrect, as it
    # won't be right if the underlying without-normals method changes at some
    # point in the future to do something different (which is pretty unlikely))
    def drawTriangle(self, v0, v1, v2, n0, n1, n2, color, alpha):
        cppyy.gbl._py_btIDebugDraw._drawTriangle(self, v0, v1, v2, color, alpha)

    # (this would be the more technically correct version)
    #def drawTriangle(self, v0, v1, v2, n0, n1, n2, color, alpha):
    #    if n0 is None:
    #        cppyy.gbl._py_btIDebugDraw._drawTriangle(self, v0, v1, v2, color, alpha)
    #    else:
    #        cppyy.gbl._py_btIDebugDraw._drawTriangle(self, v0, v1, v2, n0, n1, n2, color, alpha)

    def drawContactPoint(self, pointOnB, normalOnB, distance, lifetime, color):
        raise NotImplementedError()

    def reportErrorWarning(self, warningString):
        raise NotImplementedError()

    def draw3dText(self, location, textString):
        raise NotImplementedError()

    def setDebugMode(self, debugMode):
        raise NotImplementedError()

    def getDebugMode(self):
        raise NotImplementedError()

    drawAabb = cppyy.gbl._py_btIDebugDraw._drawAabb

    drawTransform = cppyy.gbl._py_btIDebugDraw._drawTransform

    drawArc = cppyy.gbl._py_btIDebugDraw._drawArc

    drawSpherePatch = cppyy.gbl._py_btIDebugDraw._drawSpherePatch

    drawBox = cppyy.gbl._py_btIDebugDraw._drawBox

    drawCapsule = cppyy.gbl._py_btIDebugDraw._drawCapsule

    drawCylinder = cppyy.gbl._py_btIDebugDraw._drawCylinder

    drawCone = cppyy.gbl._py_btIDebugDraw._drawCone

    drawPlane = cppyy.gbl._py_btIDebugDraw._drawPlane

